import json
import string
import time
import uuid
from typing import TypeVar

from fastapi import Request
from starlette.datastructures import MutableHeaders
from starlette.types import ASGIApp, Message, Receive, Scope, Send

from app.core.config import settings
from app.core.handlers.loggers import logger
from app.core.handlers.request_context import request_context as rc

T = TypeVar("T", bound=dict | list)


class RequestLogMiddleware:
    def __init__(self, app: ASGIApp) -> None:
        self.app = app

    @staticmethod
    def get_short_id():
        buffer = string.ascii_letters + string.digits
        _id = uuid.uuid4().hex
        short_id = ""
        for i in range(0, 8):
            start = i * 4
            end = i * 4 + 4
            val = int(_id[start:end], 16)
            short_id += buffer[val % 62]
        return short_id

    def desensitization(self, data: T) -> T:
        if settings.debug:
            return data
        if isinstance(data, dict):
            for k, v in data.items():
                if k in settings.sensitive_field and isinstance(v, str):
                    data[k] = "******"
                elif isinstance(v, dict | list):
                    data[k] = self.desensitization(v)
        elif isinstance(data, list):
            for i, value in enumerate(data):
                data[i] = self.desensitization(value)
        return data

    def desensitization_dump(self, data: T) -> str:
        return json.dumps(self.desensitization(data), ensure_ascii=False)

    @staticmethod
    def get_request_ip(request: Request) -> str:
        """
        获取客户端真实IP地址
        优先级顺序：
        1. X-Forwarded-For 头部（代理传递的真实客户端IP）
        2. X-Real-IP 头部
        3. CF-Connecting-IP 头部（Cloudflare）
        4. request.client.host（直接连接的客户端IP）
        """
        if forwarded_for := request.headers.get("x-forwarded-for"):
            if client_ip := forwarded_for.split(",")[0].strip():
                return client_ip

        if real_ip := request.headers.get("x-real-ip"):
            return real_ip

        if cf_ip := request.headers.get("cf-connecting-ip"):
            return cf_ip

        if x_original_forwarded_for := request.headers.get("x-original-forwarded-for"):
            return x_original_forwarded_for.split(",")[0].strip()

        if request.client:
            return request.client.host

        return "unknown"

    def set_request_context(self, request: Request):
        request_id = request.headers.get("HTTP_X_REQUEST_ID", self.get_short_id())
        request_ip = self.get_request_ip(request)
        request_method = request.method
        request_path = request.url.path
        request.state.request_id = request_id
        request.state.request_ip = request_ip
        request.state.request_method = request_method
        request.state.request_path = request_path
        rc.set("request_id", request_id)
        rc.set("request_ip", request_ip)
        rc.set("request_method", request_method)
        rc.set("request_path", request_path)

    async def __call__(self, scope: Scope, receive: Receive, send: Send):
        request = Request(scope, receive)
        self.set_request_context(request)
        request_data = {
            "path_params": request.path_params,
            "query_params": dict(request.query_params),
        }

        _m = await receive()

        if body := _m.get("body"):
            if length := len(body) > 1024 * 10:
                logger.warning(f"The body is too long to print, {length=}")
            else:
                try:
                    request_data["body"] = json.loads(body.decode())
                except json.JSONDecodeError:
                    logger.warning("request body is not json")
        logger.info(
            f"API Request Received, "
            f"request_data={self.desensitization_dump(request_data)}"
        )

        async def receive_wrapper():
            nonlocal _m
            return _m

        start_time = time.perf_counter()
        http_status = None
        content_type = None

        async def send_wrapper(message: Message):
            nonlocal start_time, http_status, content_type
            cost = round((time.perf_counter() - start_time) * 1000, 2)
            if message["type"] == "http.response.start":
                message["headers"].extend(
                    [
                        (b"X-Request-ID", request.state.request_id.encode("latin-1")),
                        (b"X-Response-Time", f"{cost}ms".encode("latin-1")),
                    ]
                )
                logger.debug(f"http.response.start: {message=}")
                http_status = message.get("status")
                headers = MutableHeaders(raw=message["headers"])
                content_type = headers.get("content-type")

            if message["type"] == "http.response.body":
                response_log = (
                    f"API Response Sent, {http_status=}, {cost=}ms, {content_type=}"
                )
                if content_type == "application/json" and (
                    body_str := message.get("body")
                ):
                    response = json.loads(body_str.decode())
                    response_log += f", response={self.desensitization_dump(response)}"
                logger.info(response_log)

            await send(message)

        await self.app(scope, receive_wrapper, send_wrapper)
